#!/usr/bin/python2.4
#
# Copyright 2009, Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
#     * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
#     * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""Manager for p4.

A class to help run p4 commands from python.
"""

import marshal
import subprocess


class P4Manager(object):
  """A class to manage p4."""

  def __init__(self, execute, verbose):
    object.__init__(self)
    self.execute = execute
    self.verbose = verbose
    self.p4_command = "p4"

  class Error(Exception):
    """Exception class for this module."""

    def __init__(self, message):
      Exception.__init__(self)
      self.message = message

    def __str__(self):
      return "command failed: " + self.message

  def DumpP4Output(self, p4_output):
    """Dumps the output of ExecuteP4."""
    num = 0
    for record in p4_output:
      print "\n--%d--" % num
      for key in record:
        print "%s: %s" % (key, record[key])
      num += 1

  def ExecuteP4(self, command, args, input_data, force_execute):
    """Executes p4 and returns the result as a python dict.

    Args:
      command: p4 command to execute.
      args: list of arguments to p4.
      input_data: a dictionary of inputs to p4 command. Pass in {} if you have
          no input data.
      force_execute: True means execute. False means execute if this instance
          of the P4Manager was created with execute = True.

    Returns:
      An array of dicts. Each command returns a different format of dict.

    Raises:
      Error: An error occured calling p4.
    """
    if self.verbose:
      print " ".join([self.p4_command, command] + args)

    records = []
    if self.execute or force_execute:
      p = subprocess.Popen([self.p4_command, "-G", command] + args,
                           stdout=subprocess.PIPE,
                           stdin=subprocess.PIPE)
      marshal.dump(input_data, p.stdin)
      p.stdin.close()
      try:
        while True:
          records.append(marshal.load(p.stdout))
      except EOFError:
        pass
    # not all commands return errors but this seems like a safe check for those
    # that do.
    if (records and
        "code" in records[0] and
        records[0]["code"] == "error"):
      raise P4Manager.Error(records[0]["data"])
    return records

  def ExecuteP4ToChangelist(self, changelist, command, args):
    """Executes p4 to a particular changelist.

    If no changelist exists one will be created.

    Args:
      changelist: changelist to effect
      command: p4 command to execute.
      args: list of arguments to p4.

    Returns:
      An array of dicts. Each command returns a different format of dict.

    Raises:
      Error: An error occured calling p4.
    """
    return self.ExecuteP4(command, ["-c", changelist] + args, {}, False)

  def CreateChangelist(self, description):
    """Creates a new change list.

    Once the changelist is created it is set as the changelist to be used for
    ExecuteP4ToChangelist.

    Args:
      description: Description for new changelist.

    Returns:
      changelist number as a string.

    Raises:
      Error: An error if changelist has already been created.
    """
    records = self.ExecuteP4("change", ["-o"], {}, True)
    record = records[0]
    record["Description"] = description
    # remove all the files from the default changelist.
    keys = record.keys()
    for key in keys:
      if key[0:5] == "Files":
        del record[key]
    records = self.ExecuteP4("change", ["-i"], record, False)
    if self.execute:
      # p4 change returns an array of of dicts. The first dict has a
      # "data" entry which holds the changelist number in the format
      # "Change #### created."
      changelist = records[0]["data"].split()[1]
    else:
      # if we aren't actually excuting then set changelist to something that
      # will pass for other commands.
      changelist = "????????"
    return Changelist(self, changelist)

  def Revert(self, filename):
    """p4 reverts a file.

    Args:
      filename: name of file to revert.
    """
    self.ExecuteP4("revert", [filename])

  def GetDepotPathForFolder(self, foldername):
    """Gets the depot path for the given local folder.

    Args:
      foldername: path to local folder.

    Returns:
      Corresponding Depot path of local folder.

    Raises:
      Error: An error if foldername not in current client.
    """
    records = self.ExecuteP4("dirs", [foldername], {}, True)
    if not records:
      raise P4Manager.Error("folder not in current client: " + foldername)
    return records[0]["dir"]

  def GetDepotFiles(self, depot_path):
    """Gets a list of files from a depot.

    Args:
      depot_path: Full path to a folder in the depot. Example '//depot/folder'.

    Returns:
      An array of dicts. Each dict has the fields:
        rev: The head revision number for this file.
        time: The time the action took place.
        action: The last p4 action on this file (add, edit, delete)
        type: The p4 type of the file (text, xtext, binary, ...)
        depotFile: The full path of the file in the depot.
        change: The number of the change list that as reponsible for action.
    """
    results = self.ExecuteP4("files", [depot_path + "/..."], {}, True)
    return results


class Changelist(object):
  """A class that manages a p4 changelist"""

  def __init__(self, p4_manager, changelist):
    self.p4 = p4_manager
    self.changelist = changelist

  def GetChangelistFiles(self):
    """Gets the list of files in a changelist.

    Args:
      changelist: changelist to get file list from.
    Returns:
      list of file records. Each record has fields direclty from the p4 fstat
      command. Important fields are:
        depotFile: The filename in local depot format
        path: The filename in the local file system
        action: What action is planned on this file.
        type: The type of file p4 considers the file.
    """
    records = self.p4.ExecuteP4(
        "fstat", ["-Op", "-Rc", "-e", self.changelist, "-W", "..."],
        None, True)
    # remove the last record as that's not a file.
    self.p4.DumpP4Output(records)
    file_list = records[:-1]
    return file_list

  def Revert(self):
    """Reverts all the files a changelist"""
    self.p4.ExecuteP4ToChangelist(self.changelist, "revert", ["..."])

  def Add(self, filename):
    """p4 adds a file.

    Args:
      filename: File to p4 add.
    """
    self.p4.ExecuteP4ToChangelist(self.changelist, "add", [filename])

  def Edit(self, filename):
    """p4 edits a file.

    Args:
      filename: File to p4 edit.
    """
    self.p4.ExecuteP4ToChangelist(self.changelist, "edit", [filename])

  def Delete(self, filename):
    """p4 deletes a file.

    Args:
      filename: File to p4 delete.
    """
    self.p4.ExecuteP4ToChangelist(self.changelist, "delete", [filename])

  def RevertUnchanged(self):
    """p4 reverts unchanged files in CL."""
    self.p4.ExecuteP4ToChangelist(self.changelist, "revert", ["-a"])

  def Discard(self):
    """discards a change list.

    The change list must be empty.
    """
    self.p4.ExecuteP4("change", ["-d", self.changelist], None, False)

if __name__ == "__main__":
  p4 = P4Manager(True, True)
  changelist = p4.CreateChangelist("test cl for testing")
  changelist.Add("p4manager.pyc")
  changelist.Revert()
  changelist.Discard()

